#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@NAME          : mcinsta.py
@TIME          : 2024/11/16 21:24:58
@AUTHOR        : chenlip
@VERSION       : 0.0.1
@DESCRIPTION   : minio操作类
'''
# --------------------------引用--------------------------
import os,io, datetime, urllib.parse
from minio import Minio
from minio.tagging import Tags
from minio.error import InvalidResponseError
from minio.commonconfig import REPLACE, CopySource

from app.conf.config import conf
from .utils import Utils as ut
# --------------------------引用--------------------------

class minioclient:
    def __init__(self, bucket_name:str="static", env:str="dev"):
        '''
            初始化minio客户端
            :Args:
                - bucket_name: minio中的bucket名称, 例如：static
        '''
        gconf = conf[env]
        mcserv = gconf.MINIOCONF['endpoint']
        access_key = gconf.MINIOCONF['access_key'] 
        secret_key = gconf.MINIOCONF['secret_key']

        # minio服务器地址
        isssl = True
        self._miniourl = "https://"+mcserv
        if mcserv.startswith("192.168") or mcserv.startswith("localhost"):
            self._miniourl = "http://"+mcserv
            isssl = False
        
        self._bucket_name = bucket_name
        self.mc_bk_url = self._miniourl + "/" + self._bucket_name + "/"
        # 初始化minio客户端(有认证机构颁发的 SSL证书的情况)
        self._mc = Minio(mcserv, access_key=access_key, secret_key=secret_key, secure=isssl)

        print(ut.cco("Minio server ok !" + self.mc_bk_url, "green"))

    # 根据文件后缀名称判断文件类型
    def getfiletype(self, filename:str) -> str:
        '''
            根据文件后缀名称判断文件类型
            :Args:
                - filename: 文件名称, 例如：abc.jpg
            :Return:
                - 文件类型, 例如：jpg
        '''
        return ut.getfiletype(filename)

    # 文件名转码URL
    def urlquote(self, filename:str) -> str:
        '''
            文件名转码URL
            :Args:
                - filename: 文件名称, 例如：微软黑体.ttf
            :Return:
                - 转码后的URL, 例如：%E5%BE%AE%E8%BD%AF%E9%BB%91%E4%BD%93.ttf
        '''
        return ut.urlquote(filename)
    

    # 上传本地文件到minio
    def upload_file(self, srcfile:str, miniodir:str) -> str:
        '''
            上传本地文件到minio
            :Args:
                - srcfile: 本地源文件, 例如：/home/chenlip/abc.jpg
                - miniodir: minio中的目标文件夹,必须是/结尾, 例如：2024-10-12/
            :Return:
                - minio中的目标文件, 例如：2024-10-12/abc.jpg
        '''
        # 上传文件到minio
        try:
            _mcfile = miniodir + os.path.basename(srcfile)
            self._mc.fput_object(self._bucket_name, _mcfile, srcfile)
            print(ut.cco(f"上传文件成功:{_mcfile}", "green"))
        except InvalidResponseError as err:
            print(ut.cco(f"上传文件失败，原因：{err}"))
            _mcfile = "error"
        except Exception as err:
            print(ut.cco(f"上传文件失败，原因：{err}"))
            _mcfile = "error"
        
        return _mcfile
    
    def create_dir(self, srcdir:str, miniodir:str) -> bool:
        '''
            创建minio文件夹(不是bucket)
            :Args:
                - srcdir: 在指定的目录下创建文件夹, 例如：home/chenlip/
                - miniodir: minio中的目标文件夹,必须是/结尾, 例如：src/
            :Return:
                - True/False
        '''
        _mcdir = srcdir + miniodir
        try:
            self._mc.put_object(self._bucket_name, _mcdir, io.BytesIO(b''), 0)
            # 在创建文件夹成功后，放入一个rd.txt文件, 文件内容为b'hello'
            self._mc.put_object(self._bucket_name, _mcdir + "rd.txt", io.BytesIO(b'hello', encoding='utf-8'), 5)
            print(ut.cco(f"创建文件夹成功:{miniodir}", "green"))
            return True
        except InvalidResponseError as err:
            print(ut.cco(f"创建文件夹失败，原因：{err}"))
            return False
        except Exception as err:
            print(ut.cco(f"创建文件夹失败，原因：{err}"))
            return False

    
    # 使用pub_object上传文本文件stream 到minio某个文件夹下的某个文件对象
    def upload_stream_txt(self, fncont:str, miniodir:str, filename:str) -> str:
        '''
            使用pub_object上传文本文件stream 到minio某个文件夹下的某个文件对象
            :Args:
                - stream: 文件流
                - miniodir: minio中的目标文件夹,必须是/结尾, 例如：2024-10-12/
                - filename: 文件名称, 例如：abc.md
            :Return:
                - minio中的目标文件, 例如：2024-10-12/abc.jpg
        '''
        # 上传文件到minio
        _str = io.BytesIO(fncont, encoding='utf-8')
        try:
            _mcfile = miniodir + filename
            self._mc.put_object(self._bucket_name, _mcfile, _str, length=-1, part_size=10*1024*1024)
            print(ut.cco(f"上传 文件流 成功:{_mcfile}", "green"))
        except InvalidResponseError as err:
            print(ut.cco(f"上传 文件流 失败，原因：{err}"))
            _mcfile = "error"
        except Exception as err:
            print(ut.cco(f"上传 文件流 失败，原因：{err}"))
            _mcfile = "error"
        
        return _mcfile
    
    # 获取minio文件内容（txt，json, md）
    def get_file_cont(self, srcfile:str) -> str:
        '''
            获取minio文件内容（txt，json, md）
            :Args:
                - srcfile: minio中的源文件, 例如：2024-10-12/abc.md
            :Return:
                - 文件内容
        '''
        _mcfilecont = "no"
        try:
            _mcfile = self._mc.get_object(self._bucket_name, srcfile)
            _mcfilecont = _mcfile.data.decode('utf-8')
            print(ut.cco(f"获取 文件内容 成功:{srcfile}", "green"))
        except InvalidResponseError as err:
            print(ut.cco(f"获取 文件内容 失败，原因：{err}"))
            _mcfilecont = "error"
        except Exception as err:
            print(ut.cco(f"获取 文件内容 失败，原因：{err}"))
            _mcfilecont = "error"
        
        return _mcfilecont
    
    # 下载minio文件到本地
    def download_file(self, srcfile:str, localfile:str) -> str:
        '''
            下载minio文件到本地
            :Args:
                - srcfile: minio中的源文件, 例如：2024-10-12/abc.jpg
                - localfile: 本地目标文件, 例如：/home/chenlip/abc.jpg
            :Return:
                - 本地目标文件, 例如：/home/chenlip/abc.jpg
        '''
        # 下载文件到本地
        try:
            self._mc.fget_object(self._bucket_name, srcfile, localfile)
            print(ut.cco(f"下载 {self._bucket_name}-{srcfile} 文件成功:{localfile}", "green"))
        except InvalidResponseError as err:
            print(ut.cco(f"下载文件失败，原因：{err}"))
            localfile = "error"
        except Exception as err:
            print(ut.cco(f"下载文件失败，原因：{err}"))
            localfile = "error"
        
        return localfile

    def list_obj(self, srcdir:str, ftypelst:list=None) -> list:
        '''
            获取minio中的文件列表
            :Args:
                - srcdir: minio中的源文件夹,必须是/结尾, 例如：fonts/chinafont/
            :Return:
                - 文件列表, 如果是'/'结尾，说明时文件夹。 例如：
                ["fonts/chinafont/abc.ttf", ...]
        '''
        _mcfilelist = []
        if not ftypelst:
            _f_ex = ["ttf", "otf", "woff", "woff2"]
        else:
            _f_ex = ftypelst
        if not srcdir.endswith("/"): 
            srcdir = srcdir + "/"
        
        # 获取源文件夹中的文件列表
        try:
            for obj in self._mc.list_objects(self._bucket_name, srcdir):
                
                # 如果文件对象是_f_ex中的文件后缀，则添加到_mcfilelist中
                if obj.object_name.split(".")[-1] in _f_ex:
                    _mcfilelist.append(obj.object_name)
            
            print(ut.cco(f"获取Minio文件列表数量: {len(_mcfilelist)}", "green"))
        except InvalidResponseError as err:
            print(ut.cco(f"获取{srcdir}文件列表失败，原因：{err}"))

        return _mcfilelist
    
    # 使用递归+循环mc.list_objects方法，生成某个目录下的所有目录树，返回一个树状结构的字典
    def list_obj_tree(self, srcdir:str, _mcdir=None) -> dict:
        '''
            使用递归+循环mc.list_objects方法，生成minio某个目录下的所有目录树，返回一个树状结构的字典
            注意：如果Minio对象是'/'结尾，说明是文件夹
            输入：minio中的源文件夹,必须是/结尾, 例如：fonts/chinafont/
            最终输出：目录树字典
            :Args:
                - srcdir: minio中的源文件夹,必须是/结尾, 例如：fonts/chinafont/
            :Return:
                - 数据字典 例如：
                如果该目录下面没有子目录，则返回空字典
                {
                    "目录1": {},
                    "目录2": {
                        "目录2.1": {},
                        "目录2.2": {},
                        "目录2.3": {
                            "目录2.3.1": {},
                            "目录2.3.2": {},
                        }
                    },
                    "目录3": {},
                }
        '''
        if _mcdir is None:
            _mcdir = {}

        if not srcdir.endswith("/"):
            srcdir += "/"

        for obj in self._mc.list_objects(self._bucket_name, prefix=srcdir, recursive=False):
            obj_name = obj.object_name[len(srcdir):]
            if obj_name.endswith("/"):
                folder_name = obj_name.rstrip("/")
                _mcdir[folder_name] = {}
                self.list_obj_tree(srcdir + folder_name + "/", _mcdir[folder_name])
            else:
                # 如果是文件，不处理
                continue

        return _mcdir
    
    # 在minio对象的tags中添加标签
    def addTags(self, srcfile:str, tagdict:dict) -> bool:
        '''
            在minio对象的tags中添加标签, Minio BUG：tagValue不能为中文
            :Args:
                - srcfile: minio中的源文件, 例如：fonts/chinafont/abc.ttf
                - tagdict: 标签字典, 例如：{"refname": "custfont01", "size": 2800}
            :Return:
                - True/False
        '''
        tags = Tags.new_object_tags()
        for k, v in tagdict.items():
            tags[k] = v

        try:
            self._mc.set_object_tags(self._bucket_name, srcfile, tags)
            print(ut.cco("添加/修改标签成功！如果该对象已经存在相同的key的标签，则替换成新的值，如果不存在则新增", "green"), tagdict)
            return True
        except Exception as err:
            print(ut.cco(f"添加/修改标签失败，原因：{err}"))
            return False
    
    # 获取某个对象的标签
    def getTags(self, srcfile:str, tagkey:str=None) -> dict:
        '''
            获取某个对象的标签
            :Args:
                - srcfile: minio中的源文件, 例如：fonts/chinafont/abc.ttf
                - tagkey: 标签key, 例如：refname, 如果为空则返回所有标签，否则返回指定key的标签的值
            :Return:
                - 标签字典, 例如：{"refname": "custfont01", "size": 2800}
        '''
        try:
            _tagdict = self._mc.get_object_tags(self._bucket_name, srcfile)
            if tagkey:
                return {tagkey, _tagdict[tagkey]}
            else:
                return _tagdict
        except Exception as err:
            print(ut.cco(f"获取标签失败，原因：{err}"))
            return {}
        
    # 删除某个对象的标签
    def delTags(self, srcfile:str) -> bool:
        '''
            删除某个对象的标签
            :Args:
                - srcfile: minio中的源文件, 例如：fonts/chinafont/abc.ttf
            :Return:
                - True/False
        '''
        try:
            self._mc.delete_object_tags(self._bucket_name, srcfile)
            print(ut.cco(f"删除对象所有标签成功！{srcfile}", "green"))
            return True
        except Exception as err:
            print(ut.cco(f"删除对象所有标签成功，原因：{err}"))
            return False
    
    def getObjMeta(self, srcfile:str) -> dict:
        '''
            获取minio中的文件的元数据
            :Args:
                - srcfile: minio中的源文件, 例如：fonts/chinafont/abc.ttf
            :Return:
                - 文件内容元数据, 
        '''
        _meta = {
                "refname": "no",
                "srcfile": "微软黑体.ttf",
                "encodename": "微软黑体.ttf",
                "type": "opentype",
                "size": 2800,
                "ctime": "2019-10-09",
        }
        obj_meta = self._mc.stat_object(self._bucket_name, srcfile)
        obj_tags = self.getTags(srcfile)

        _ttfname = obj_meta.object_name.split("/")[-1]
        _meta["miniobase"] = self.mc_bk_url
        _meta["srcdir"] = srcfile.replace(_ttfname, "")
        _meta["srcfile"] = _ttfname
        _meta["encodename"] = self.urlquote(_ttfname)
        _meta["type"] = self.getfiletype(_ttfname)
        _meta["size"] = round(obj_meta.size / (1024 * 1024), 1)
        _meta["ctime"] = obj_meta.last_modified.strftime('%Y-%m-%d')
        _meta["tags"] = obj_tags

        print(ut.cco("获取文件元数据成功！", "blue"), _ttfname, _meta["encodename"], _meta["size"], _meta["ctime"])

        return _meta
    
    # 判断minio中的文件是否存在
    def isExistObj(self, srcfile:str) -> bool:
        '''
            判断minio中的文件是否存在
            :Args:
                - srcfile: minio中的源文件, 例如：fonts/chinafont/abc.ttf
            :Return:
                - True/False
        '''
        try:
            obj = self._mc.stat_object(self._bucket_name, srcfile)
            if obj:
                return True
            else:
                return False
        except Exception as err:
            return False
    
